\clearpage

\section{WebSocket 服务器与客户端设计}

本章将介绍 WebSocket 协议的细节以及实现思路，并展示如何将 WebSocket 协议与 Socket 服务器客户端结合成为 WebSocket 服务器客户端。

\subsection{协议原理}

\subsubsection{WebSocket 握手}

根据 RFC 6455 所描述的 WebSocket 端点行为，Socket 连接达成后，双方均处于 Connecting 状态。WebSocket 客户端需要按照指定的格式向服务器发送握手信息，握手信息必须为正确的 HTTP 请求，使用 GET 方法和大于等于 1.1 的 HTTP 版本，其中 HTTP Message 部分要求如下：

\begin{itemize}[topsep=5pt]
  \setlength{\itemindent}{0pt}
        \setlength{\itemsep}{0pt}
        \setlength{\parsep}{0pt}
        \setlength{\parskip}{0pt}
  \item 必须包含 \texttt{Host} 字段，其值为服务器地址
  \item 必须包含 \texttt{Upgrade} 字段，其值必须包含 \texttt{websocket} 关键字
  \item 必须包含 \texttt{Connection} 字段，其值必须包含 \texttt{Upgrade}
  \item 必须包含 \texttt{Sec-WebSocket-Key} 字段，其值必须为随机生成的 16 字节数据进行 base64 编码后的字符串。每次连接的值必须为随即生成的
  \item 必须包含 \texttt{Sec-WebSocket-Version} 字段，其值必须为 13
  \item 可以包含 \texttt{Sec-WebSocket-Protocol} 等其它字段
\end{itemize}

在完整接收到客户端的请求后（HTTP 请求头部以两组回车换行作为与 Body 的分割），WebSocket 服务器需要对此 HTTP 请求进行检验，包括请求的资源是否存在以及请求是否满足 WebSocket 格式，如果成功则需要返回一个握手的回应，其必须为正确的 HTTP 回应，在状态码为 101 的情况下，HTTP Message 部分需要满足：
\begin{itemize}[topsep=5pt]
  \setlength{\itemindent}{0pt}
        \setlength{\itemsep}{0pt}
        \setlength{\parsep}{0pt}
        \setlength{\parskip}{0pt}
  \item 必须包含 \texttt{Upgrade} 字段，其值必须为 \texttt{websocket}
  \item 必须包含 \texttt{Connection} 字段，其值必须包含 \texttt{Upgrade}
  \item 必须包含 \texttt{Sec-WebSocket-Accept} 字段，其值必须为：\texttt{Sec-WebSocket-Key} 与字符串 \texttt{"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"} 拼接后使用 SHA1 哈希并用 base64 进行编码得到的字符串
  \item 如果包含 \texttt{Sec-WebSocket-Protocol} 字段则其值必须在 WebSocket 连接请求中相应字段中出现过。
\end{itemize}

如果服务器或客户端检测到对方的握手信息有误，则连接断开。在客户端检测服务器的 WebSocket 握手回应成功后则 WebSocket 连接正式成立，双方的 WebSocket 处于 Open 状态。

\subsubsection{WebSocket 数据传送与装帧}

在连接建立后，双方便可以进行双工的通信了。考虑到 WebSocket 底层的 TCP 连接传输的为字节流，WebSocket 将双方的数据装帧以获得更友好的协议语义。数据帧主要由三个部分组成，Opcode、载荷长度和应用数据，具体的结构如图 \ref{websocket-frame-structure}。标准规定，客户端发往服务器的帧必须经过掩蔽，而服务器发往客户端的帧必须不经过掩蔽。

\begin{figure}[htbp]
  \centering
  \includegraphics[width=.9\linewidth]{final/websocket-frame-structure.png}
  \caption{WebSocket 帧结构}
  \label{websocket-frame-structure}
\end{figure}

帧的性质由第一个字节决定。FIN 表示该帧是否为结束帧，（首帧也可能为结束帧）；RSV1 - RSV3 在没有定义 WebSocket 扩展时必须为 0；opcode 表示帧的行为和性质，其值有以下几种：

\begin{itemize}[topsep=5pt]
  \setlength{\itemindent}{0pt}
        \setlength{\itemsep}{0pt}
        \setlength{\parsep}{0pt}
        \setlength{\parskip}{0pt}
  \item 0 : 代表是上一个发送的帧的延续
  \item 1 : 当前帧为文本帧
  \item 2 : 当前帧为二进制帧
  \item 3-7 为保留的非控制帧
  \item 8 : 代表连接关闭的控制帧 opcode
  \item 9 : Ping 控制帧
  \item 10: Pong 控制帧
  \item 11-15 为保留的控制帧 opcode
\end{itemize}

考虑到载荷的大小差距可能比较悬殊，WebSocket 设计了变长的 载荷长度 字段，当载荷长度小于 126 字节时，则只用一个字节来代表载荷长度；处于 126 - 65535 字节时，额外使用 2 个字节表示载荷长度；大于 65536 字节的额外使用 8 个字节代表载荷长度（使用网络字节序）。第二个字节的第一位代表该 WebSocket 帧是否被掩蔽，如果为是则会在载荷长度后添加 4 个字节的掩码。
从此开始的载荷长度字节数则为应用数据。

\par

客户端在发送数据时需要对帧进行掩蔽。它首先生成随机的 32 位整数，并写入帧中，然后每 8 位轮流将载荷与掩码进行异或操作。客户端需要保证掩码的不可预测性，否则会对数据的安全性产生影响。

\par

WebSocket 支持分帧的主要目的是为了让发送数据的一方能够按照部分发送一份完整的数据或者是一份未知大小的数据，这样只需要维持固定大小的输出缓冲区。同时分帧还能支持多路复用，防止一份大量的数据传输完全占用了输出通道，而不能用来发送控制信号等。在传送分为若干帧的数据中途，WebSocket 端点能够发送/接收控制帧并立即对控制帧做出响应。

\subsubsection{WebSocket 控制}

WebSocket 标准规定了三种控制帧， Ping 、Pong、和关闭连接。Ping 是用来测试连接是否仍然成立，接收到 Ping 控制帧的一方需要立即返回一个 Pong ，其载荷要与收到的 Ping 控制帧中的载荷一致。发送 Ping 的一方在超过一定时间没有收到回应的话可以认为连接已经断开。在连续收到多个 Ping 控制帧时，可以选择只回应最后一个。如果连接的一方需要一个单向的心跳包，则可以直接向对方发送 Pong 。

\par

WebSocket 连接断开需要进行一次关闭连接握手。希望关闭连接的一方发送出一个 Close 控制帧，同时将 WebSocket 状态切换为 Closing，此时无法再发送数据。当对方收到 Close 控制帧时，在当前数据发送完成后再向对方返回一个 Close 控制帧，并将状态变为 Closed。关闭握手发起的一方收到对方的 CLose 控制帧后也进入 Closed 状态，此时双方应该关闭 WebSocket 底层的 TCP 连接。

\subsection{基础设施设计}

\subsubsection{握手部分}

WebSocket 握手部分使用的是 HTTP 协议，请求和回应都包含 HTTP Message 部分，我们设计了 \texttt{handshake\_request}、\texttt{handshake\_response}、\texttt{http\_message} 用来保存握手的信息，以及配套的解析器以从字节流中解析出握手信息。其主要成员与接口如图 \ref{websocket-handshake-classes} 所示。

\begin{figure}[htbp]
  \centering
  \includegraphics[width=.65\linewidth]{final/websocket-handshake-classes.png}
  \caption{WebSocket 握手相关类}
  \label{websocket-handshake-classes}
\end{figure}

握手的解析实现为这三个类的静态方法。其函数签名如代码片段 \ref{websocket-handshake-parser} 所示，注意到它们返回值类型为 \texttt{std::variant<std::string, \_>} 。其中\texttt{std::variant} 用来表示 C++ 中的和类型，即返回值可能是 \texttt{std::variant} 模板类参数中的一种。即解析方法返回的结果可能是解析成功后的数据结构，也可能是一个 \texttt{std::string}，在这里的语义代表一个异常信息。解析握手请求和回应的函数参数为 \texttt{nonstd::span<char>}，它和 \texttt{std::string\_view} 类似，用来为所有权不归属于自己的容器提供一个只读（对于容器本身而言）的副本。\texttt{std::span} 将在 C++20 中实装，考虑到稳定性等问题，项目中使用了基于 C++17 实现的非官方版本。

\begin{lstlisting}[
  captionpos=b,
  caption={WebSocket 握手解析方法签名}, 
  label={websocket-handshake-parser}
]
auto parse_http_message(std::istream& input) 
  -> std::variant<std::string, http_message>;
auto parse_handshake_request(nonstd::span<char> buf)
  -> std::variant<std::string, handshake_request>;
auto parse_handshake_response(nonstd::span<char> buf)
  -> std::variant<std::string, handshake_response>；
\end{lstlisting}

HTTP 请求和相应的头部都主要以空格字符作为分割，我们只需要相应地读取方法、请求资源、HTTP 版本或者 HTTP 响应码、理由和 HTTP 版本，并再调用 HTTP Message 的解析器即可。 \texttt{http\_message} 中提供了检测某个字段的方法，并将在检测握手信息是否有效时使用。

\subsubsection{WebSocket 帧}

WebSocket 帧使用类 \texttt{websocket\_frame} 来表示，其成员与 \ref{websocket-frame-structure} 一致，并使用单独的方法导出二进制表示。对于 WebSocket 帧的解析函数实现为该类的两个静态方法，其签名分别如代码片段 \ref{websocket-frame-parser} 所示。

\begin{lstlisting}[
  captionpos=b,
  caption={WebSocket 帧解析方法签名}, 
  label={websocket-frame-parser}
]
auto parse_websocket_frame(nonstd::span<char> buf)
  -> std::variant<
    std::string, 
    size_t, 
    std::pair<websocket_frame, size_t>
  >;

auto parse_multiple_websocket_frame(nonstd::span<char> buf)
  -> std::variant<
    std::string, 
    std::tuple<std::vector<websocket_frame>, size_t, size_t>
  >;
\end{lstlisting}

\texttt{parse\_websocket\_frame} 接收一段 \texttt{char} 缓冲区，返回一个 \texttt{std::variant}。返回值一共有三种类型，\texttt{std::string} 代表解析出错，返回异常信息；\texttt{size\_t} 表明当前数据不足以解析为一个完整的 WebSocket 帧，仍需多少字节才能获取更多的信息；\texttt{std::pair<websocket\_frame, size\_t>} 表明解析成功，结果包括一个解析出来的 WebSocket 帧和该帧的总大小。

\par

而 \texttt{parse\_multiple\_websocket\_frame} 功能类似，也接收一段 \texttt{char} 缓冲区，返回值共有两种类型。 \texttt{std::string} 代表解析出错，返回异常信息；\texttt{std::tuple} 代表解析成功，第一个成员为解析出来的若干个 WebSocket 帧，第二个成员代表一共解析了多少字节，第三个成员代表获取更多的解析信息还需要多少字节的数据。

\par

考虑到实际里需要应用到的 WebSocket 帧解析场景：WebSocket 端点接收来自对方的字节流，并将字节流解析为帧。WebSocket 接收缓冲区被设计用来对接收到的数据进行处理，并使用构造时传入的帧接收函数来对解析并拼接完成的 WebSocket 帧进行处理。在 \texttt{websocket\_receive\_buffer} 维护以下内容：未成功解析的数据、解析完成但没有结束帧的数据、距离获取下一步数据仍需要的字节数和完整帧处理函数。具体的数据接收流程如图 \ref{websocket-receiver-process}。

\begin{figure}[htbp]
  \centering
  \includegraphics[width=.9\linewidth]{final/websocket-receiver-process.png}
  \caption{WebSocket 接收缓冲区工作流程}
  \label{websocket-receiver-process}
\end{figure}

\subsection{WebSocket 服务器设计}

WebSocket 服务器将在 WebSocket 基础设施和 Socket 服务器的基础上实现，通过注册 Socket 服务器的钩子函数来对 WebSocket 事件进行响应。

\subsubsection{WebSocket 状态转移}

WebSocket 端点一共有 4 种状态，初始时为 CONNECTING，握手完成后变为 OPEN，此时可以自由收发数据。当一方发送关闭请求时，其状态变为 CLOSING 并不再能发送数据；收到关闭请求的一方可以先将未发送的数据发送完后发送关闭回应并进入 CLOSING 状态。随后双方关闭底层的 TCP 连接并进入 CLOSED 状态，代表连接彻底关闭。具体状态转移如图 \ref{websocket-state}。

\begin{figure}[htbp]
  \centering
  \includegraphics[width=.5\linewidth]{final/websocket-state.png}
  \caption{WebSocket 状态转移}
  \label{websocket-state}
\end{figure}

\subsubsection{WebSocket 连接类设计}

WebSocket 服务器使用 \texttt{websocket\_connection} 来维护每个 WebSocket 连接的信息。\texttt{websocket\_connection} 中记录 WebSocket 连接状态、握手数据缓冲区、WebSocket 接收数据缓冲区、连接的 Socket 远端的地址等。WebSocket 服务器使用 \texttt{std::vector<std::unique\_ptr<\_>>} 来维护这些信息，并使用 \texttt{std::mutex} 进行同步。WebSocket 连接类构造时向 WebSocket 数据接收缓冲区传入一个引用了自身对 WebSocket 帧进行处理的

\par

WebSocket 服务注册 Socket 服务器的 Accept 钩子，在其中创建新的 WebSocket 连接；注册 Recv 钩子，将数据转发给 WebSocket 连接中的 WebSocket 数据接收缓冲区，若出现错误或是接收到了关闭连接请求则由 Socket 服务器关闭底层 Socket 连接，通过注册的 Close 钩子释放掉 WebSocket 连接资源。工作流程如图 \ref{websocket-connection-process} 所示。

\begin{figure}[htbp]
  \centering
  \includegraphics[width=.95\linewidth]{final/websocket-connection-process.png}
  \caption{WebSocket 连接类工作流程}
  \label{websocket-connection-process}
\end{figure}

\subsubsection{断线检测功能设计}

由于 WebSocket 可能是依赖于代理而实现的，所以不能单纯地用 Socket 连接的通断判断对方是否离线。Ping 和 Pong 控制帧正是为此而设计出来的。我们为 WebSocket 服务器添加了守护线程，守护线程会定期检查每个连接上次获取数据的时间距离当前是否超过了阈值 1，对超时的线程将发送一个 Ping 并在阈值 2 之后认定对方已离线并关闭该连接。

\par

我们使用 \texttt{std::condition\_variable} 和 \texttt{std::mutex} 来对守护线程进行控制。守护线程使用 \texttt{std::condition\_variable} 的 \texttt{wait\_for} 方法来实现定时的检查，在关闭服务器时可以获取该条件变量对应的互斥量锁将睡眠中的守护线程唤醒并退出。

\subsection{WebSocket 客户端设计}

WebSocket 的客户端实现与服务器思路类似。区别主要在于以下几点

\begin{itemize}[topsep=5pt]
  \setlength{\itemindent}{0pt}
        \setlength{\itemsep}{0pt}
        \setlength{\parsep}{0pt}
        \setlength{\parskip}{0pt}
  \item WebSocket 客户端的实现 \texttt{websocket\_client} 为模板类，其模板类参数为 Socket 客户端的实现。我们单独定义了 \texttt{secure\_websocket\_client} 为模板参数为 \texttt{secure\_socket\_client} 的别名。
  \item WebSocket 客户端发起握手请求需要生成 160 位的随机数，在接收回应时需要将服务器发送的 \texttt{Sec-WebSocket-Accept} 与期望结果对比，出现错误则断开连接。
  \item 客户端会主动定时向服务器发送心跳包，心跳包中的数据包含发送时的时间；借助 WebSocket Pong 帧会返回 Ping 帧内容的特点，客户端可以根据接收到的 Pong 帧计算出与服务器之间的时延。
  \item WebSocket 客户端提供了发送数据和发送数据帧的两种接口，直接发送数据的接口会根据数据的大小进行分块发送；发送数据帧的接口是为了让用户拥有发送部分数据的能力。
\end{itemize}